module etc.dleveldb.leveldb;

import deimos.leveldb.c;

import etc.dleveldb.options;
import etc.dleveldb.snapshot;
import etc.dleveldb.iterator;
import etc.dleveldb.slice;

import std.traits;
import std.conv:to;


import std.string:toStringz,fromStringz;
import std.exception;
import std.logger;

unittest
{
    import std.file:tempDir,mkdirRecurse,rmdirRecurse;

    string tempPath = tempDir ~ `/dleveldb`;

    trace("start.");
    auto opt = new Options;
    opt.create_if_missing = true;
    auto db = new LevelDB(opt, tempPath ~ `s1`);
    assert(db.isOpen);
    int i = 1234;
    db.put("simple", i);
    assert(i == 1234);
    i = 0;
    db.get("simple", i);
    double x = 3.145;
    assert(i == 1234);
    db.put("simple", x);
    x = 0;
    db.get("simple", x);
    assert(x == 3.145, x.to!string);
    db.destroy;
}



class LevelDB
{
    this(string dbpath)
    {
        auto opt = new Options();
        this(opt,dbpath);
    }

    this(Options opt,string dbpath)
    {
        this.close();

        char* errptr = null;

        scope(failure) if(errptr) leveldb_free(errptr);
        
        this._db = leveldb_open(opt.ptr, dbpath.toStringz,&errptr);
        //if(errptr)
        enforce(errptr == null, "Failed to open LevelDB: " ~ errptr.fromStringz);
    }

    ~this()
    {
        this.close();
    }

    @property
    bool isOpen() const
    {
        return (_db != null);
    }

    @property
    void close()
    {
        if(this.isOpen())
        {
            leveldb_close(this._db);
            this._db = null;
        }
    }
    void put(K,V)(in K key,in V value,const(WriteOptions) opt = DefaultWriteOptions)
    {
        enforce(this.isOpen(), "Database is not open");
        char* errptr = null;
        scope(failure) if(errptr) leveldb_free(errptr);
        leveldb_put(this._db, opt.ptr, 
            key._lib_obj_ptr__,key._lib_obj_size__,
            value._lib_obj_ptr__,value._lib_obj_size__,
            &errptr
        );
        enforce(errptr == null, "Failed to put key-value pair: " ~ errptr.fromStringz);
    }

    bool get(K,V)(in K key,out V value,const(ReadOptions) opt = DefaultReadOptions)
    {
        enforce(this.isOpen(), "Database is not open");
        char* errptr = null;
        scope(failure) if(errptr) leveldb_free(errptr);
        size_t vallen = 0;
        auto valptr = leveldb_get(this._db, opt.ptr, 
            key._lib_obj_ptr__,key._lib_obj_size__,
            &vallen,
            &errptr
        );
        enforce(errptr == null, "Failed to get value: " ~ errptr.fromStringz);

        if(valptr is null)
        {
            return false;
        }

        static if(isSomeString!V || isArray!V)
	    {
            value = cast(V)(cast(char[])(valptr)[0..vallen]).dup;
	    }
        else static if(is(V == class))
        {
            enforce(typeid(V).sizeof <= vallen, "Assignment size is larger then data size" ~ typeid(V).sizeof.to!string ~ " > " ~ vallen.to!string);
            value = *(cast(V*)valptr).dup;
        }
        else
        {
            enforce(V.sizeof <= vallen, "Assignment size is not equal to data size" ~ V.sizeof.to!string ~ " != " ~ vallen.to!string);
            value = *(cast(V*)valptr);
        }
        return true;
    }

    void del(K)(in K key,const(WriteOptions) opt = DefaultWriteOptions)
    {
        enforce(this.isOpen(), "Database is not open");
        char* errptr = null;
        scope(failure) if(errptr) leveldb_free(errptr);

        leveldb_delete(this._db, opt.ptr, 
            key._lib_obj_ptr__,key._lib_obj_size__,
            &errptr
        );
        enforce(errptr == null, "Failed to delete key: " ~ errptr.fromStringz);
    }

    

    

    private:
    leveldb_t* _db;

    public:

    class DBSnapshot : Snapshot
    {
        this()
        {
            enforce(
                this._snapshot = cast(leveldb_snapshot_t*)leveldb_create_snapshot(this.outer._db),
                "Failed to create snapshot"
            );
        }
        ~this()
        {
            if(this.isValid && this.outer.isOpen())
            {
                leveldb_release_snapshot(this.outer._db, this._snapshot);
            }
            this._snapshot = null;
        }
    }

    class DBIterator : Iterator
    {
        this(const(ReadOptions) opt = DefaultReadOptions)
        {
            enforce(
                this._iterator = leveldb_create_iterator(this.outer._db, opt.ptr),
                "Failed to create iterator"
            );
        }

        ~this()
        {
            if(this._iterator !is null){
                if(this.outer.isOpen())
                {
                    leveldb_iter_destroy(this._iterator);
                }
                this._iterator = null;
            }
        }
    }

    public:
    static void destroyDB(const(Options) opt,string dbpath)
    {
        char* errptr = null;
        scope(exit) if(errptr) leveldb_free(errptr);
        leveldb_destroy_db(opt.ptr, dbpath.toStringz, &errptr);
        enforce(errptr == null, "Failed to destroy LevelDB: " ~ errptr.fromStringz);
    }

    static void repairDB(const(Options) opt,string dbpath)
    {
        char* errptr = null;
        scope(exit) if(errptr) leveldb_free(errptr);
        leveldb_repair_db(opt.ptr, dbpath.toStringz, &errptr);
        enforce(errptr == null, "Failed to repair LevelDB: " ~ errptr.fromStringz);
    }

    static auto majorVersion() => leveldb_major_version();
    static auto minorVersion() => leveldb_minor_version();

}